Celovit vodnik po TypeScript generikih, ki zajema njihovo sintakso, prednosti, napredno uporabo in najboljše prakse za delo s kompleksnimi podatkovnimi tipi.
TypeScript generiki: obvladovanje kompleksnih podatkovnih tipov za robustne aplikacije
TypeScript, nadmnožica JavaScripta, razvijalcem omogoča pisanje bolj robustne in vzdržljive kode s pomočjo statičnega tipiziranja. Med njegovimi najmočnejšimi funkcijami so generiki, ki omogočajo pisanje kode, ki deluje z različnimi podatkovnimi tipi, hkrati pa ohranja tipovno varnost. Ta vodnik ponuja celovito raziskovanje TypeScript generikov, s poudarkom na njihovi uporabi pri kompleksnih podatkovnih tipih v kontekstu globalnega razvoja programske opreme.
Kaj so generiki?
Generiki omogočajo pisanje ponovno uporabne kode, ki lahko deluje z različnimi tipi. Namesto da bi pisali ločene funkcije ali razrede za vsak tip, ki ga želite podpreti, lahko napišete eno samo funkcijo ali razred, ki uporablja tipske parametre. Ti tipski parametri so ograde za dejanske tipe, ki bodo uporabljeni ob klicu ali instanciranju funkcije ali razreda. To je še posebej uporabno pri delu s kompleksnimi podatkovnimi strukturami, kjer se lahko tip podatkov znotraj teh struktur razlikuje.
Prednosti uporabe generikov
- Ponovna uporaba kode: Kodo napišete enkrat in jo uporabite z različnimi tipi. To zmanjša podvajanje kode in naredi vašo kodno bazo bolj vzdržljivo.
- Tipovna varnost: Generiki omogočajo prevajalniku TypeScript, da zagotovi tipovno varnost med prevajanjem. To pomaga preprečevati napake med izvajanjem, povezane z neujemanjem tipov.
- Izboljšana berljivost: Generiki naredijo vašo kodo bolj berljivo, saj jasno označujejo tipe, s katerimi so vaše funkcije in razredi zasnovani za delo.
- Povečana zmogljivost: V nekaterih primerih lahko generiki vodijo do izboljšav zmogljivosti, ker lahko prevajalnik optimizira generirano kodo na podlagi uporabljenih specifičnih tipov.
Osnovna sintaksa generikov
Osnovna sintaksa generikov vključuje uporabo ostrih oklepajev (< >) za deklaracijo tipskih parametrov. Ti tipski parametri so običajno poimenovani T
, K
, V
itd., vendar lahko uporabite kateri koli veljaven identifikator. Tukaj je preprost primer generične funkcije:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);
console.log(myString); // Izhod: hello
console.log(myNumber); // Izhod: 123
console.log(myBoolean); // Izhod: true
V tem primeru <T>
deklarira tipski parameter z imenom T
. Funkcija identity
sprejme argument tipa T
in vrne vrednost tipa T
. Ob klicu funkcije lahko eksplicitno določite tipski parameter (npr. identity<string>
) ali pa pustite, da ga TypeScript sklepa na podlagi tipa argumenta.
Delo s kompleksnimi podatkovnimi tipi
Generiki postanejo še posebej dragoceni pri delu s kompleksnimi podatkovnimi tipi, kot so tabele, objekti in vmesniki. Poglejmo si nekaj pogostih scenarijev:
Generične tabele
Generike lahko uporabite za ustvarjanje funkcij ali razredov, ki delujejo s tabelami različnih tipov:
function arrayToString<T>(arr: T[]): string {
return arr.join(", ");
}
let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];
console.log(arrayToString(numberArray)); // Izhod: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Izhod: apple, banana, cherry
Tukaj funkcija arrayToString
sprejme tabelo tipa T[]
in vrne nizovno predstavitev tabele. Ta funkcija deluje s tabelami katerega koli tipa, kar jo naredi zelo ponovno uporabno.
Generični objekti
Generike lahko uporabimo tudi za definiranje funkcij ali razredov, ki delujejo z objekti različnih oblik:
interface Person {
name: string;
age: number;
country: string; // Dodana država za globalni kontekst
}
interface Product {
id: number;
name: string;
price: number;
currency: string; // Dodana valuta za globalni kontekst
}
function displayInfo<T extends { name: string }>(item: T): void {
console.log(`Name: ${item.name}`);
}
let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };
displayInfo(person); // Izhod: Name: Alice
displayInfo(product); // Izhod: Name: Laptop
V tem primeru funkcija displayInfo
sprejme objekt tipa T
, ki mora imeti lastnost name
tipa string. Ključna beseda extends { name: string }
je omejitev, ki določa minimalne zahteve za tipski parameter T
. To zagotavlja, da lahko funkcija varno dostopa do lastnosti name
.
Napredna uporaba generikov
TypeScript generiki ponujajo naprednejše funkcije, ki vam omogočajo ustvarjanje še bolj prilagodljive in zmogljive kode. Poglejmo si nekatere od teh funkcij:
Več tipskih parametrov
Definirate lahko funkcije ali razrede z več tipskimi parametri:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
interface Name {
firstName: string;
}
interface Age {
age: number;
}
const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };
const merged = merge(person, details);
console.log(merged.firstName); // Izhod: Bob
console.log(merged.age); // Izhod: 42
Funkcija merge
sprejme dva objekta tipov T
in U
ter vrne nov objekt, ki vsebuje lastnosti obeh objektov. To je zmogljiv način za združevanje podatkov iz različnih virov.
Generične omejitve
Kot smo že videli, omejitve omogočajo omejevanje tipov, ki jih je mogoče uporabiti z generičnim tipskim parametrom. To zagotavlja, da lahko generična koda varno deluje na določenih tipih.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity([1, 2, 3]); // Izhod: 3
loggingIdentity("hello"); // Izhod: 5
// loggingIdentity(123); // Napaka: Argument tipa 'number' ni mogoče dodeliti parametru tipa 'Lengthwise'.
Funkcija loggingIdentity
sprejme argument tipa T
, ki mora imeti lastnost length
tipa number. To zagotavlja, da lahko funkcija varno dostopa do lastnosti length
.
Generični razredi
Generike lahko uporabimo tudi z razredi:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(d => d !== item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Izhod: [ 'banana' ]
const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Izhod: [ 2 ]
Razred DataStorage
lahko shranjuje podatke katerega koli tipa T
. To vam omogoča ustvarjanje ponovno uporabnih podatkovnih struktur, ki so tipovno varne.
Generični vmesniki
Generični vmesniki so uporabni za definiranje pogodb, ki lahko delujejo z različnimi tipi. Na primer:
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
interface User {
id: number;
username: string;
email: string;
}
interface ErrorMessage {
code: number;
message: string;
}
function fetchUser(id: number): Result<User, ErrorMessage> {
if (id === 1) {
return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
} else {
return { success: false, error: { code: 404, message: "User not found" } };
}
}
const userResult = fetchUser(1);
if (userResult.success) {
console.log(userResult.data.username);
} else {
console.log(userResult.error.message);
}
Vmesnik Result
definira generično strukturo za predstavitev rezultata operacije. Vsebuje lahko bodisi podatke tipa T
bodisi napako tipa E
. To je pogost vzorec za obravnavo asinhronih operacij ali operacij, ki lahko spodletijo.
Pomožni tipi in generiki
TypeScript ponuja več vgrajenih pomožnih tipov, ki dobro delujejo z generiki. Ti pomožni tipi vam lahko pomagajo pri preoblikovanju in manipulaciji tipov na zmogljive načine.
Partial<T>
Partial<T>
naredi vse lastnosti tipa T
opcijske:
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: "Alice" }; // Veljavno
Readonly<T>
Readonly<T>
naredi vse lastnosti tipa T
samo za branje (readonly):
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Napaka: Ni mogoče dodeliti vrednosti 'age', ker je lastnost samo za branje.
Pick<T, K>
Pick<T, K>
izbere nabor lastnosti K
iz tipa T
:
interface Person {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };
Omit<T, K>
Omit<T, K>
odstrani nabor lastnosti K
iz tipa T
:
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };
Record<K, T>
Record<K, T>
ustvari tip s ključi K
in vrednostmi tipa T
:
type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Razširjen seznam za globalni kontekst
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Razširjen seznam za globalni kontekst
type CurrencyMap = Record<CountryCodes, Currency>;
const currencyMap: CurrencyMap = {
"US": "USD",
"CA": "CAD",
"UK": "GBP",
"DE": "EUR",
"FR": "EUR",
"JP": "JPY",
"CN": "CNY",
"IN": "INR",
"BR": "BRL",
"AU": "AUD",
};
Preslikani tipi
Preslikani tipi vam omogočajo preoblikovanje obstoječih tipov z iteracijo po njihovih lastnostih. To je zmogljiv način za ustvarjanje novih tipov na podlagi obstoječih. Na primer, lahko ustvarite tip, ki naredi vse lastnosti drugega tipa samo za branje:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Napaka: Ni mogoče dodeliti vrednosti 'age', ker je lastnost samo za branje.
V tem primeru [K in keyof Person]
iterira po vseh ključih vmesnika Person
, Person[K]
pa dostopa do tipa vsake lastnosti. Ključna beseda readonly
naredi vsako lastnost samo za branje.
Pogojni tipi
Pogojni tipi omogočajo definiranje tipov na podlagi pogojev. To je zmogljiv način za ustvarjanje tipov, ki se prilagajajo različnim scenarijem.
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string
function getValue<T>(value: T): NonNullable<T> {
if (value == null) { // Obravnava tako null kot undefined
throw new Error("Vrednost ne more biti null ali undefined");
}
return value as NonNullable<T>;
}
try {
const validValue = getValue("hello");
console.log(validValue.toUpperCase()); // Izhod: HELLO
const invalidValue = getValue(null); // To bo sprožilo napako
console.log(invalidValue); // Ta vrstica ne bo dosežena
} catch (error: any) {
console.error(error.message); // Izhod: Vrednost ne more biti null ali undefined
}
V tem primeru tip NonNullable<T>
preveri, ali je T
enak null
ali undefined
. Če je, vrne never
, kar pomeni, da tip ni dovoljen. V nasprotnem primeru vrne T
. To vam omogoča ustvarjanje tipov, ki zagotovo ne morejo biti null.
Najboljše prakse za uporabo generikov
Tukaj je nekaj najboljših praks, ki jih je dobro upoštevati pri uporabi generikov:
- Uporabljajte opisna imena tipskih parametrov: Izberite imena, ki jasno kažejo na namen tipskega parametra.
- Uporabljajte omejitve za omejevanje tipov, ki se lahko uporabljajo z generičnim tipskim parametrom: To zagotavlja, da lahko vaša generična koda varno deluje na določenih tipih.
- Ohranjajte svojo generično kodo preprosto in osredotočeno: Izogibajte se prekomernemu zapletanju vaše generične kode s preveč tipskimi parametri ali kompleksnimi omejitvami.
- Temeljito dokumentirajte svojo generično kodo: Pojasnite namen tipskih parametrov in morebitne uporabljene omejitve.
- Upoštevajte kompromise med ponovno uporabo kode in tipovno varnostjo: Čeprav lahko generiki izboljšajo ponovno uporabo kode, lahko vašo kodo tudi naredijo bolj kompleksno. Pred uporabo generikov pretehtajte prednosti in slabosti.
- Upoštevajte lokalizacijo in globalizacijo (l10n in g11n): Pri delu s podatki, ki jih je treba prikazati uporabnikom v različnih regijah, zagotovite, da vaši generiki podpirajo ustrezno oblikovanje in kulturne konvencije. Na primer, oblikovanje števil in datumov se lahko med različnimi lokalizacijami močno razlikuje.
Primeri v globalnem kontekstu
Poglejmo si nekaj primerov, kako se lahko generiki uporabljajo v globalnem kontekstu:
Pretvorba valut
interface ConversionRate {
rate: number;
fromCurrency: string;
toCurrency: string;
}
function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
return amount * rate.rate;
}
const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD je enako ${amountInEUR} EUR`); // Izhod: 100 USD je enako 85 EUR
Oblikovanje datumov
interface DateFormatOptions {
locale: string;
options: Intl.DateTimeFormatOptions;
}
function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
return date.toLocaleDateString(format.locale, format.options);
}
const currentDate = new Date();
const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };
console.log("US Date: " + formatDate(currentDate, usDateFormat));
console.log("German Date: " + formatDate(currentDate, germanDateFormat));
console.log("Japanese Date: " + formatDate(currentDate, japaneseDateFormat));
Prevajalska storitev
interface Translation {
[key: string]: string; // Omogoča dinamične jezikovne ključe
}
interface LanguageData<T extends Translation> {
languageCode: string;
translations: T;
}
const englishTranslations: Translation = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our website!"
};
const spanishTranslations: Translation = {
"hello": "Hola",
"goodbye": "Adiós",
"welcome": "¡Bienvenido a nuestro sitio web!"
};
const frenchTranslations: Translation = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre site web !"
};
const languageData: LanguageData<typeof englishTranslations>[] = [
{languageCode: "en", translations: englishTranslations },
{languageCode: "es", translations: spanishTranslations },
{languageCode: "fr", translations: frenchTranslations}
];
function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
const lang = languageData.find(lang => lang.languageCode === languageCode);
if (!lang) {
return `Prevod za ${key} v jeziku ${languageCode} ni bil najden.`;
}
return lang.translations[key] || `Prevod za ${key} ni bil najden.`;
}
console.log(translate("hello", "en", languageData)); // Izhod: Hello
console.log(translate("hello", "es", languageData)); // Izhod: Hola
console.log(translate("welcome", "fr", languageData)); // Izhod: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Izhod: Prevod za missingKey v jeziku de ni bil najden.
Zaključek
TypeScript generiki so zmogljivo orodje za pisanje ponovno uporabne, tipovno varne kode, ki lahko deluje s kompleksnimi podatkovnimi tipi. Z razumevanjem osnovne sintakse, naprednih funkcij in najboljših praks generikov lahko znatno izboljšate kakovost in vzdržljivost vaših TypeScript aplikacij. Pri razvoju aplikacij za globalno občinstvo vam lahko generiki pomagajo pri obravnavi različnih podatkovnih formatov in kulturnih konvencij, kar zagotavlja brezhibno uporabniško izkušnjo za vse.